<?php

declare(strict_types=1);

namespace Upside\Std;

final class Str implements \Stringable
{
    public const string CONTROL_CHARACTERS = " \t\r\n\f\v";

    private string $raw;

    public function __construct(string $raw = '')
    {
        $this->raw = $raw;
    }

    #[\Override]
    public function __toString(): string
    {
        return $this->raw;
    }

    public static function from(string $raw = ''): Str
    {
        return new self($raw);
    }

    public function length(): int
    {
        return \strlen($this->raw);
    }

    public function starts_with(\Stringable|string $needle): bool
    {
        return \str_starts_with($this->raw, $needle);
    }

    public function ends_with(\Stringable|string $needle): bool
    {
        return \str_ends_with($this->raw, $needle);
    }

    public function contains(\Stringable|string $needle): bool
    {
        return \str_contains($this->raw, $needle);
    }

    public function split(string $separator = ' ', int $limit = \PHP_INT_MAX): StrArr
    {
        return new StrArr(\explode($separator, $this->raw, $limit));
    }

    public function wrap(\Stringable|string $prefix, \Stringable|string|null $suffix = null): Str
    {
        $suffix = $suffix ?? $prefix;

        return self::from($prefix . $this->raw . $suffix);
    }

    /**
     * @param string[]|\Stringable[]|StrArr $strings
     */
    public function join(StrArr|array $strings): Str
    {
        return self::from(\implode($this->raw, $strings instanceof StrArr ? $strings->raw() : $strings));
    }

    /**
     * @param string[]|\Stringable[]|StrArr $strings
     */
    public function join_wrap(StrArr|array $strings, \Stringable|string $prefix, \Stringable|string|null $suffix = null): Str
    {
        if (!$strings instanceof StrArr) {
            $strings = new StrArr($strings);
        }

        return $this->join(
            $strings->map(
                static fn(\Stringable|string $item) => (self::from($item))->wrap($prefix, $suffix)
            )
        );
    }

    public function upper(): self
    {
        return self::from(\strtoupper($this->raw));
    }

    public function upper_first(): self
    {
        return self::from(\ucfirst($this->raw));
    }

    public function lover(): self
    {
        return self::from(\strtolower($this->raw));
    }

    public function capitalize(): self
    {
        return $this->lover()->upper_first();
    }

    public function title(\Stringable|string $separators = self::CONTROL_CHARACTERS): Str
    {
        return self::from(\ucwords($this->raw, $separators));
    }

    public function replace(\Stringable|string $search, \Stringable|string $replace, ?int &$count = null): Str
    {
        $result = null === $count ?
            \str_replace($search, $replace, $this->raw) :
            \str_replace($search, $replace, $this->raw, $count);

        return self::from($result);
    }

    public function count(\Stringable|string $needle, int $offset = 0, ?int $length = null): int
    {
        return \substr_count($this->raw, $needle, $offset, $length);
    }

    public function index(\Stringable|string $needle, int $offset = 0): int
    {
        return \strpos($this->raw, $needle, $offset);
    }

    public function trim(\Stringable|string $characters = self::CONTROL_CHARACTERS): Str
    {
        return self::from(\trim($this->raw, $characters));
    }

    public function ltrim(\Stringable|string $characters = self::CONTROL_CHARACTERS): Str
    {
        return self::from(\ltrim($this->raw, $characters));
    }

    public function rtrim(\Stringable|string $characters = self::CONTROL_CHARACTERS): Str
    {
        return self::from(\rtrim($this->raw, $characters));
    }

    public function is_empty(): bool
    {
        return '' === $this->raw;
    }
}
